package cn.jimmie.lib.qlog.interceptor;

import java.io.File;
import java.io.RandomAccessFile;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.util.concurrent.BlockingDeque;
import java.util.concurrent.LinkedBlockingDeque;

import cn.jimmie.lib.qlog.QLogLevel;
import cn.jimmie.lib.qlog.support.AlByte;
import cn.jimmie.lib.qlog.support.AlFile;
import cn.jimmie.lib.qlog.support.AlIO;

/**
 * Func : mmap内存映射 文件日志拦截器
 *
 * <p> Create Time:  2019-12-18 10:51 </p>
 * <p> @author Jimmie.Qian  </p>
 */
public class MmapFileInterceptor implements FileInterceptor, Runnable {
    private static final int BUFF_SIZE = 64 * 1024;
    private static final int FLUSH_SIZE = 48 * 1024;
    private static final byte LINE = '\n';

    private final BlockingDeque<String> queue = new LinkedBlockingDeque<>();
    private volatile boolean quit = false;

    private byte[] buff = new byte[BUFF_SIZE];
    private boolean enabled = true;
    private MappedByteBuffer mbb;
    private HeaderInterceptor header;
    private int position = 0;
    private FileChannel fileChannel;

    private File logFile;

    @Override
    public void init(HeaderInterceptor header) {
        this.header = header;
        try {
            String mmapPath = header.options().context().getExternalFilesDir("") + File.separator + "qlog_mmap";
            fileChannel = new RandomAccessFile(new File(mmapPath), "rw").getChannel();
            mbb = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, BUFF_SIZE);

            new Thread(this, "mmap-file-thread").start();
        } catch (Throwable ignored) {
        }
    }

    @Override
    public boolean enabled() {
        return enabled;
    }

    @Override
    public void enabled(boolean enable) {
        this.enabled = enable;
    }

    @Override
    public void submit(QLogLevel level, String message) {
        if (mbb == null || level.level < header.options().level().level) return;
        queue.offer(message);
    }

    @Override
    public void release() {
        AlIO.close(fileChannel);
        if (mbb != null) mbb.clear();
        quit = true;
        queue.clear();
    }

    private void adjustPosition() {
        if (mbb == null) return;
        fetchBuff();
        int p = -1;
        for (int i = buff.length - 1; i > 0; i--) {
            byte b = buff[i];
            if (b != '\0') {
                p = i;
                break;
            }
        }
        position = ++p;
        mbb.position(position);
    }

    private void resetBuff() {
        if (mbb == null) return;
        AlByte.recycle(buff);
        mbb.position(0);
        mbb.put(buff);
        mbb.position(0);
        position = 0;
    }

    private int fetchBuff() {
        if (mbb == null) return 0;
        mbb.position(0);
        mbb.get(buff);
        int p = position;
        position = 0;
        return p;
    }

    @Override
    public void run() {
        if (mbb == null) return;
        try {
            adjustPosition();

            mbb.put(LINE);
            mbb.put(header.fixedInfo().getBytes());
            mbb.put(LINE);
            position = mbb.position();

            flushBuff();

            while (!quit) {
                String message = queue.take();
                mbb.put(message.getBytes());
                mbb.put(LINE);
                position = mbb.position();

                if (position >= FLUSH_SIZE) {
                    flushBuff();
                }
            }
        } catch (Throwable ignored) {
        }
    }

    private void flushBuff() {
        checkLogFile();
        int size = fetchBuff();
        AlFile.writeAppend(logFile, buff, 0, size);
        resetBuff();
    }

    private void checkLogFile() {
        if (logFile == null) logFile = new File(header.options().logPath());
        if (logFile.length() > header.options().logMaxSize()) {
            try {
                boolean delete = logFile.delete();
                if (delete) logFile = new File(header.options().logPath());
            } catch (Throwable ignore) {
            }
        }
    }
}
